**Постановка задачи**
Ваша задача — провести оценку результатов A/B-теста. В вашем распоряжении есть датасет с действиями пользователей, техническое задание и несколько вспомогательных датасетов.
Чтобы оценить корректность проведения теста, проверьте:
**Техническое задание**
recommender_system_test;product_page,product_cart,purchase.**Данные**
ab_project_marketing_events.csv
**Описание данных**
ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год.
Структура файла:
name — название маркетингового события;regions — регионы, в которых будет проводиться рекламная кампания;start_dt — дата начала кампании;finish_dt — дата завершения кампании.final_ab_new_users.csv — пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года.
Структура файла:
user_id — идентификатор пользователя;first_date — дата регистрации;region — регион пользователя;device — устройство, с которого происходила регистрация.final_ab_events.csv — действия новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
Структура файла:
user_id — идентификатор пользователя;event_dt — дата и время покупки;event_name — тип события;details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.final_ab_participants.csv — таблица участников тестов.
Структура файла:
user_id — идентификатор пользователя;ab_test — название теста;group — группа пользователя.**Оглавление**
Примечание:
# Загружаем библиотеки
import pandas as pd
from urllib.parse import urlencode
import requests
import numpy as np
import seaborn as sns
import datetime as dt
from scipy import stats as st
import math as mth
from datetime import date, datetime, timedelta
from matplotlib import pyplot as plt
from plotly import graph_objects as go
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')
#Напишем функцию
def datasets (base_url, public_key):
final_url = base_url + urlencode(dict(public_key=public_key))
response = requests.get(final_url)
download_url = response.json()['href']
dataset = pd.read_csv(download_url)
return dataset
#Выведим данные таблицы final_ab_participants
base_url = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?'
public_key = 'https://disk.yandex.ru/d/nhGUR3ZkNIG4VA'
final_ab_participants = datasets (base_url, public_key)
final_ab_participants
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
| ... | ... | ... | ... |
| 18263 | 1D302F8688B91781 | B | interface_eu_test |
| 18264 | 3DE51B726983B657 | A | interface_eu_test |
| 18265 | F501F79D332BE86C | A | interface_eu_test |
| 18266 | 63FBE257B05F2245 | A | interface_eu_test |
| 18267 | 79F9ABFB029CF724 | B | interface_eu_test |
18268 rows × 3 columns
#Выведим данные таблицы final_ab_events
base_url = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?'
public_key = 'https://disk.yandex.ru/d/pgKgZ9lRp0Enwg'
final_ab_events = datasets (base_url, public_key)
final_ab_events
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
| ... | ... | ... | ... | ... |
| 440312 | 245E85F65C358E08 | 2020-12-30 19:35:55 | login | NaN |
| 440313 | 9385A108F5A0A7A7 | 2020-12-30 10:54:15 | login | NaN |
| 440314 | DB650B7559AC6EAC | 2020-12-30 10:59:09 | login | NaN |
| 440315 | F80C9BDDEA02E53C | 2020-12-30 09:53:39 | login | NaN |
| 440316 | 7AEC61159B672CC5 | 2020-12-30 11:36:13 | login | NaN |
440317 rows × 4 columns
#Выведим данные таблицы ab_project_marketing_events
base_url = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?'
public_key = 'https://disk.yandex.ru/d/ZVC7GUYYckKnGQ'
ab_project_marketing_events = datasets (base_url, public_key)
ab_project_marketing_events
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
#Выведим данные таблицы final_ab_new_users
base_url = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?'
public_key = 'https://disk.yandex.ru/d/Qrto8Gerpu424g'
final_ab_new_users = datasets (base_url, public_key)
final_ab_new_users
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
| ... | ... | ... | ... | ... |
| 61728 | 1DB53B933257165D | 2020-12-20 | EU | Android |
| 61729 | 538643EB4527ED03 | 2020-12-20 | EU | Mac |
| 61730 | 7ADEE837D5D8CBBD | 2020-12-20 | EU | PC |
| 61731 | 1C7D23927835213F | 2020-12-20 | EU | iPhone |
| 61732 | 8F04273BB2860229 | 2020-12-20 | EU | Android |
61733 rows × 4 columns
# Выведим основную информацию о датафреймах с помощью метода `info()`,`head()`,`describe()`,`shape`
# с помощью цикла
for data in [final_ab_events,
ab_project_marketing_events,
final_ab_new_users,
final_ab_participants]:
display(data.head(),
data.info(),
data.describe().T,
data.shape)
display('****************************************************')
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
None
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| details | 62740.0 | 23.877631 | 72.180465 | 4.99 | 4.99 | 4.99 | 9.99 | 499.99 |
(440317, 4)
'****************************************************'
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
None
| count | unique | top | freq | |
|---|---|---|---|---|
| name | 14 | 14 | Christmas&New Year Promo | 1 |
| regions | 14 | 6 | APAC | 4 |
| start_dt | 14 | 14 | 2020-12-25 | 1 |
| finish_dt | 14 | 14 | 2021-01-03 | 1 |
(14, 4)
'****************************************************'
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
None
| count | unique | top | freq | |
|---|---|---|---|---|
| user_id | 61733 | 61733 | D72A72121175D8BE | 1 |
| first_date | 61733 | 17 | 2020-12-21 | 6290 |
| region | 61733 | 4 | EU | 46270 |
| device | 61733 | 4 | Android | 27520 |
(61733, 4)
'****************************************************'
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
None
| count | unique | top | freq | |
|---|---|---|---|---|
| user_id | 18268 | 16666 | 0FDFDA0B2DEC2D91 | 2 |
| group | 18268 | 2 | A | 9655 |
| ab_test | 18268 | 2 | interface_eu_test | 11567 |
(18268, 3)
'****************************************************'
Изменим типы данных с датами на тип datetime
final_ab_events['event_dt']= pd.to_datetime(final_ab_events['event_dt'], format = '%Y-%m-%d')
ab_project_marketing_events ['start_dt']= pd.to_datetime(ab_project_marketing_events ['start_dt'], format = '%Y-%m-%d')
ab_project_marketing_events ['finish_dt']= pd.to_datetime(ab_project_marketing_events ['finish_dt'], format = '%Y-%m-%d')
final_ab_new_users['first_date']= pd.to_datetime(final_ab_new_users['first_date'], format = '%Y-%m-%d')
Проверим изменения
for data in [final_ab_events,
ab_project_marketing_events,
final_ab_new_users]:
display(data.info())
display('****************************************************')
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null datetime64[ns] 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: datetime64[ns](1), float64(1), object(2) memory usage: 13.4+ MB
None
'****************************************************'
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null datetime64[ns] 3 finish_dt 14 non-null datetime64[ns] dtypes: datetime64[ns](2), object(2) memory usage: 576.0+ bytes
None
'****************************************************'
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null datetime64[ns] 2 region 61733 non-null object 3 device 61733 non-null object dtypes: datetime64[ns](1), object(3) memory usage: 1.9+ MB
None
'****************************************************'
for data in [final_ab_events,
ab_project_marketing_events,
final_ab_new_users,
final_ab_participants]:
display(data.duplicated().sum(), data.isna().sum())
display('****************************************************')
0
user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
'****************************************************'
0
name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
'****************************************************'
0
user_id 0 first_date 0 region 0 device 0 dtype: int64
'****************************************************'
0
user_id 0 group 0 ab_test 0 dtype: int64
'****************************************************'
Проверим датафрейм final_ab_events, где обаружены пропуски
display(final_ab_events['details'].value_counts())
display(final_ab_events['event_name'].value_counts())
4.99 46362 9.99 9530 99.99 5631 499.99 1217 Name: details, dtype: int64
login 189552 product_page 125563 purchase 62740 product_cart 62462 Name: event_name, dtype: int64
**Наблюдение:** Столбец delailsсодержит 377 577 пропусков, заполним их пропусками, тк это доп данные по событию purchase.
final_ab_events['details'] = final_ab_events['details'].fillna(0)
# Проверим
final_ab_events.isna().sum()
user_id 0 event_dt 0 event_name 0 details 0 dtype: int64
**Техническое задание**
recommender_system_test;product_page,product_cart,purchase.Во времея проведения EDA нам нужно ответить на следующие вопросы:
Проверим, что время проведения теста не совпадает с маркетинговыми и другими активностями из таблицы ab_project_marketing_events
ab_project_marketing_events[np.logical_and(ab_project_marketing_events['start_dt'] > '2020-12-07', \
ab_project_marketing_events['finish_dt'] < '2021-04-01')]
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
**Наблюдение:** В период проведения теста проводились маркетинговые активности Christmas&New Year Promo и CIS New Year Gift Lottery .
#Посмотрим распределение пользователей по тестам и по группам
final_ab_participants.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'}).reset_index()
| ab_test | group | user_id | |
|---|---|---|---|
| 0 | interface_eu_test | A | 5831 |
| 1 | interface_eu_test | B | 5736 |
| 2 | recommender_system_test | A | 3824 |
| 3 | recommender_system_test | B | 2877 |
В тесте recommender_system_test имееется две группы. В группу А попали 3824 пользователя, а в группу В 2877.
#проверим соответствие дат регистрации техническому заданию
print('Минимальная дата:', final_ab_new_users['first_date'].min())
print('Максимальная дата:', final_ab_new_users['first_date'].max())
Минимальная дата: 2020-12-07 00:00:00 Максимальная дата: 2020-12-23 00:00:00
В ТЗ указано, что дата остановки набора новых пользователей: 2020-12-21. А в наших данных, как мы видим, есть регистрации и 23.12.2020. Удалим лишние записи.
# Создадим датафрэйм только у теми пользователеми, которые были зарегистрованы на тест в это промежуток
final_ab_new_users = final_ab_new_users.query ('first_date <="2020-12-21"')
final_ab_new_users['first_date'].max()
Timestamp('2020-12-21 00:00:00')
Дата остановки теста: 2021-01-04, согласно ТЗ. Проверим на корректность даты в таблице с событиями
print('Минимальная дата:', final_ab_events['event_dt'].min())
print('Максимальная дата:', final_ab_events['event_dt'].max())
Минимальная дата: 2020-12-07 00:00:33 Максимальная дата: 2020-12-30 23:36:33
Последняя дата событий в данных 30 декабря. Хотя тест должен был продолжаться до 4 января. Возможно тест завершили раньше.
Пользователи, зарегистрировавшиеся после 16 декабря не "прошли" весь лайфтайм в 14 дней, и не в полной мере показали как внесенные изменения повлияли на их активность. Это может сильно исказить результаты тестирования.
print('Количество новых пользователей', final_ab_new_users['first_date'].count())
print('Количество пользователей, участвующих в тесте', final_ab_participants['user_id'].nunique())
Количество новых пользователей 56470 Количество пользователей, участвующих в тесте 16666
Посмотрим количество аудитории: 15% новых пользователей из региона EU по датафрейму, где только с период с 07 по 21.12.2020
# Объеденим таблицы методом merge по user_id
users = final_ab_participants.merge(final_ab_new_users, on = 'user_id', how = 'left').dropna()
users.shape
(17266, 6)
print('Пользователи из Европы, принявшие участие в тесте',\
len(users.query('ab_test == "recommender_system_test" and region == "EU"')))
print('Всего зарегистрировалось пользователей из Европы',len(final_ab_new_users.query('region == "EU"')))
print('Процент пользователей из Европы, принявших участие в тесте', len(users.query('ab_test == "recommender_system_test" and region == "EU"'))\
/len(final_ab_new_users.query('region == "EU"'))*100)
Пользователи из Европы, принявшие участие в тесте 6351 Всего зарегистрировалось пользователей из Европы 42340 Процент пользователей из Европы, принявших участие в тесте 15.0
Данные соответствуют ТЗ, тк доля пользователей из Европы 15%
Проверим правильность распределения участников теста
Согласно таблице final_ab_participants параллельно проводилось два теста, наша задача исключить задвоение участников, которые были в двух тестах.
# Сгруппируем данные по пользователю и группе
dupl_user = users.groupby('user_id', as_index=False).agg({'ab_test': 'nunique'})
#dupl_user.columns = ['user_id', 'group', 'count']
# Сделаем сред по количеству
dupl_user = dupl_user.query('ab_test > 1').sort_values(by='user_id')
print('Количество пользователей, которые участвовали в двух тестах {} или {:.1%}'
.format(dupl_user['user_id'].count(), dupl_user['user_id'].count()/final_ab_participants['user_id'].count()))
Количество пользователей, которые участвовали в двух тестах 1602 или 8.8%
dupl_user.head(3)
| user_id | ab_test | |
|---|---|---|
| 2 | 001064FEAAB631A1 | 2 |
| 10 | 00341D8401F0F665 | 2 |
| 12 | 003B6786B4FF5B03 | 2 |
# Перепроверим любого пользователя на участие в друх выборках
display(final_ab_participants.query('user_id == "001064FEAAB631A1"'))
display(final_ab_participants.query('user_id == "003B6786B4FF5B03"'))
| user_id | group | ab_test | |
|---|---|---|---|
| 235 | 001064FEAAB631A1 | B | recommender_system_test |
| 17892 | 001064FEAAB631A1 | B | interface_eu_test |
| user_id | group | ab_test | |
|---|---|---|---|
| 3156 | 003B6786B4FF5B03 | A | recommender_system_test |
| 8143 | 003B6786B4FF5B03 | A | interface_eu_test |
Пользователей, принявших участие в двух тестах одновременно 1602 или 8,8%. Для чистоты эксперемента необходимо удалить этих пользователей, так как при дальнейшем анализе они могут исказить конечный результат. Ведь мы точно не можем сказать, какой из экспериментов повлиял на принятие того или иного решения пользователя.
print('Всего пользователей БЫЛО', users['user_id'].nunique())
print(users.groupby('ab_test').agg({'user_id': 'nunique'}))
Всего пользователей БЫЛО 15664
user_id
ab_test
interface_eu_test 10565
recommender_system_test 6701
#удаляем дубликаты
list = dupl_user['user_id']
users = users.query('user_id not in @list')
print('Всего пользователей СТАЛО', users['user_id'].nunique())
print(users.groupby('ab_test').agg({'user_id': 'nunique'}))
Всего пользователей СТАЛО 14062
user_id
ab_test
interface_eu_test 8963
recommender_system_test 5099
В итоге после удаления у нас осталось всего 14062 уникальных пользователя, из которых 5099 попали в recommender_system_test. Для дальнейшей работы выделим данные по тесту recommender_system_test в отдельный датафрейм и проверим на пересечение пользователей в группах.
#оставляем данные о тесте recommender_system_test
recommender_system_test = users.query('ab_test == "recommender_system_test"')
recommender_system_test['ab_test'].unique()
recommender_system_test.sample(5)
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 1808 | 7B646640B70CCFF4 | A | recommender_system_test | 2020-12-12 | EU | Android |
| 5061 | E659A40ADD2D4E31 | B | recommender_system_test | 2020-12-21 | EU | PC |
| 1554 | 6F1E79FE8F5795CF | B | recommender_system_test | 2020-12-14 | EU | Android |
| 2151 | E53E3F40AE9433F9 | B | recommender_system_test | 2020-12-19 | EU | Android |
| 2726 | 4F8F5E43B8BD003D | A | recommender_system_test | 2020-12-12 | EU | Android |
#смотрим распределение по группам
recommender_system_test.groupby('group').agg({'user_id': 'nunique'}).reset_index()
| group | user_id | |
|---|---|---|
| 0 | A | 2903 |
| 1 | B | 2196 |
# Сгруппируем данные по пользователю и группе
dupl_group = recommender_system_test.groupby('user_id', as_index=False).agg({'ab_test': 'nunique'}).reset_index()
dupl_group.columns = ['user_id', 'group', 'count']
# Сделаем сред по количеству
dupl_group = dupl_group.query('count > 1').sort_values(by='user_id')
print('Количество пользователей, которые участвовали в двух группах {} или {:.1%}'
.format(dupl_group['user_id'].count(), dupl_group['user_id'].count()/final_ab_participants['user_id'].count()))
Количество пользователей, которые участвовали в двух группах 0 или 0.0%
Пересечений пользователей по группам нет.
Посмотрим равномерное распределение по группам
recommender_system_test['group'].value_counts()
A 2903 B 2196 Name: group, dtype: int64
Группы распределены не одинаково. В группе А присутствует 2903 пользователей (57%), а в группе В - 2196 пользователей (43%)
Посмотрим на горизонт проведения теста в 14 дней и удалим не нужное
Согласно нашему ТЗ наш тест происходил в промежуток с 2020-12-07 по 2020-12-21, присоеденим данные о событиях пользователей к имеющемуся тесту
#присоединяем события
user_event = recommender_system_test.merge(final_ab_events, on = 'user_id', how = 'left')
display(user_event.head(), user_event.info())
<class 'pandas.core.frame.DataFrame'> Int64Index: 21115 entries, 0 to 21114 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 21115 non-null object 1 group 21115 non-null object 2 ab_test 21115 non-null object 3 first_date 21115 non-null datetime64[ns] 4 region 21115 non-null object 5 device 21115 non-null object 6 event_dt 18804 non-null datetime64[ns] 7 event_name 18804 non-null object 8 details 18804 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 1.6+ MB
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 14:43:27 | purchase | 99.99 |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-25 00:04:56 | purchase | 4.99 |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 14:43:29 | product_cart | 0.00 |
| 3 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-25 00:04:57 | product_cart | 0.00 |
| 4 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 14:43:27 | product_page | 0.00 |
None
#удалим пользователей без событий, они нам без надобности
df_AB_testing = user_event.dropna(subset = ['event_name'])
len(user_event)
21115
# удаляем события, совершенные после лайфтайма
df_AB_testing = (df_AB_testing.drop(df_AB_testing[df_AB_testing['event_dt'] >= (df_AB_testing['first_date'] + pd.Timedelta(14, 'D'))].index))
df_AB_testing.shape
(18154, 9)
После того, как мы нашли участников тестирования и зарегистрированных участников в нужный промежуток времени, мы объединили таблицы, удалили пользователей без событий и удалили события, которые не вошли в указанный промежуток.
Можно приступить к основному анализу данных.
Посмотрим, сколько уникальных пользователей входит в каждую группу.
AB_testing_unique_users = df_AB_testing.groupby('group')['user_id'].agg(['count']).reset_index()
AB_testing_unique_users['%'] = ((AB_testing_unique_users['count']/AB_testing_unique_users['count'].sum())*100).round(1)
AB_testing_unique_users.sort_values(by='%', ascending=False)
AB_testing_unique_users.style.bar(subset=['%'], color='#ffe135')
| group | count | % | |
|---|---|---|---|
| 0 | A | 14340 | 79.000000 |
| 1 | B | 3814 | 21.000000 |
Посмотрим на гистограмме распределение количества событий на одного пользователя в каждой группе
# Сгруппируем данные
events_users_count = df_AB_testing.groupby(['user_id', 'group'], as_index=False).agg({'event_name' : 'count'})
events_users_count.columns = ['user_id', 'group', 'events']
events_users_count.query('group == ["A","B"] & events > 0')\
.groupby('group')['events']\
.plot(kind='hist', bins=len(events_users_count['events'].unique()), alpha=0.5,\
figsize=(15,8))
plt.legend(['"группа "А"','"группа "B"'])
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.title('Гистограмма распределения количества событий на одного пользователя')
plt.show()
Посмотрим, как происходило распределение событий на одного пользователя в выборках
print('В среднем на пользователя приходится %d событий' % (df_AB_testing['user_id'].value_counts().mean()))
print('По медиане на пользователя приходится %d событий' % (df_AB_testing['user_id'].value_counts().median()))
print('По моде на пользователя приходится %d событий' % (df_AB_testing['user_id'].value_counts().mode()))
В среднем на пользователя приходится 6 событий По медиане на пользователя приходится 6 событий По моде на пользователя приходится 6 событий
В группе В на одного пользователя чаще всего приходится от 1 до 6 событий. В группе А чаще совершают событий от 2 до 12.
#смотрим описательную статистику распределения событий в группе А
df_AB_testing.query('group == "A"').groupby('user_id')['event_name'].count().describe()
count 2082.000000 mean 6.887608 std 3.824835 min 1.000000 25% 4.000000 50% 6.000000 75% 9.000000 max 24.000000 Name: event_name, dtype: float64
#смотрим описательную статистику распределения событий в группе B
df_AB_testing.query('group == "B"').groupby('user_id')['event_name'].count().describe()
count 706.000000 mean 5.402266 std 3.256364 min 1.000000 25% 3.000000 50% 4.000000 75% 6.000000 max 24.000000 Name: event_name, dtype: float64
#распределение количества участников по группам
df_AB_testing.groupby('group')['user_id'].nunique()
group A 2082 B 706 Name: user_id, dtype: int64
ВЫВОД:
Посчитаем, как число событий распределено по дням. Сгруппируем таблицу df_AB_testing по дню события и группе A/B-теста, посчитаем количество пользователей. Сохраним результат в переменную df_AB_testing_daily.
# Добавим в датафрэйм столбец с датой
df_AB_testing['event_day'] = df_AB_testing['event_dt'].dt.date
df_AB_testing.sample(5)
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details | event_day | |
|---|---|---|---|---|---|---|---|---|---|---|
| 4597 | D493E891D8F64BBC | A | recommender_system_test | 2020-12-14 | EU | Android | 2020-12-22 02:30:08 | login | 0.0 | 2020-12-22 |
| 7947 | 0DA942E5D1BB9A1F | A | recommender_system_test | 2020-12-16 | EU | Mac | 2020-12-22 06:48:37 | product_page | 0.0 | 2020-12-22 |
| 15867 | 8CB19D3BF05472EB | A | recommender_system_test | 2020-12-19 | EU | Mac | 2020-12-19 21:22:07 | login | 0.0 | 2020-12-19 |
| 18823 | 9073CEFFB95784BA | A | recommender_system_test | 2020-12-08 | EU | Mac | 2020-12-09 19:01:05 | login | 0.0 | 2020-12-09 |
| 5124 | 796235FF6E88D357 | A | recommender_system_test | 2020-12-07 | EU | Android | 2020-12-18 16:34:45 | login | 0.0 | 2020-12-18 |
# Посчитаем кол-во событий по дням
df_AB_testing['event_day'].value_counts()
2020-12-21 1854 2020-12-20 1442 2020-12-19 1408 2020-12-18 1189 2020-12-22 1137 2020-12-17 1136 2020-12-16 1101 2020-12-14 1040 2020-12-15 1008 2020-12-23 867 2020-12-24 723 2020-12-09 569 2020-12-25 540 2020-12-07 524 2020-12-27 468 2020-12-10 461 2020-12-26 461 2020-12-08 450 2020-12-12 436 2020-12-11 404 2020-12-13 368 2020-12-28 312 2020-12-29 256 Name: event_day, dtype: int64
df_AB_testing_daily = df_AB_testing.groupby(['event_day', 'group'])\
.agg({'event_name':'count'})\
.reset_index()
df_AB_testing_daily.head(30)
| event_day | group | event_name | |
|---|---|---|---|
| 0 | 2020-12-07 | A | 234 |
| 1 | 2020-12-07 | B | 290 |
| 2 | 2020-12-08 | A | 267 |
| 3 | 2020-12-08 | B | 183 |
| 4 | 2020-12-09 | A | 296 |
| 5 | 2020-12-09 | B | 273 |
| 6 | 2020-12-10 | A | 265 |
| 7 | 2020-12-10 | B | 196 |
| 8 | 2020-12-11 | A | 282 |
| 9 | 2020-12-11 | B | 122 |
| 10 | 2020-12-12 | A | 282 |
| 11 | 2020-12-12 | B | 154 |
| 12 | 2020-12-13 | A | 253 |
| 13 | 2020-12-13 | B | 115 |
| 14 | 2020-12-14 | A | 823 |
| 15 | 2020-12-14 | B | 217 |
| 16 | 2020-12-15 | A | 834 |
| 17 | 2020-12-15 | B | 174 |
| 18 | 2020-12-16 | A | 810 |
| 19 | 2020-12-16 | B | 291 |
| 20 | 2020-12-17 | A | 927 |
| 21 | 2020-12-17 | B | 209 |
| 22 | 2020-12-18 | A | 976 |
| 23 | 2020-12-18 | B | 213 |
| 24 | 2020-12-19 | A | 1180 |
| 25 | 2020-12-19 | B | 228 |
| 26 | 2020-12-20 | A | 1193 |
| 27 | 2020-12-20 | B | 249 |
| 28 | 2020-12-21 | A | 1537 |
| 29 | 2020-12-21 | B | 317 |
# Построим график
fig = px.bar(df_AB_testing_daily, x="event_day", y= 'event_name', color="group", title="Распределение событий по группам в разрезе дней",\
#text_auto=True,
pattern_shape_sequence=["+"],\
labels= {'event_day': 'Дата', 'event_name': 'Количество событий'},
width=1000, # указываем размеры графика
height=500)
fig.show()
**Наблюдение:** Основная активность двух групп в разрезе событий пришлась на 21 декабря 2020 года. В этот день обе группы имеют максимальные значения.
Посмотрим отдельно распределение каждого события в каждой группе по дням
# Сделаем срез по группам
group_A = df_AB_testing.query('group == "A"')
group_B = df_AB_testing.query('group == "B"')
# Сгруппируем события группы "А" по дням
group_A.pivot_table(index='event_day', values='user_id',columns='event_name',aggfunc='count')\
.plot.bar(stacked=True,figsize=(12,6))
plt.title("Группа A")
plt.show()
# Сгруппируем события группы "B" по дням
group_B.pivot_table(index='event_day', values='user_id',columns='event_name',aggfunc='count')\
.plot.bar(stacked=True,figsize=(12,6))
plt.title("Группа B")
plt.show()
ВЫВОД:
Этапы воронки продаж
1. Login - пользователь входит на сайт;\
2. Product_page - предложение о товаре (экран с товаром);\
3. Product_card - переход в корзину;\
4. Purshase - экран успешной оплаты заказа.\
Этапы Product_card и Purshase имеют разное количество заходов пользователей, причем покупок больще чем переходов в корзину. Следовательно покупки совершаются по одному товару миную корзину. Надо расставить события в нужный порядок.
Посчитаем общее количество событий по всем этапам
# Сгруппируем данные по событиям
steps = df_AB_testing.groupby('event_name').agg({'event_name': ['count'], 'user_id': ['nunique'] }).reset_index()
steps.columns = ('event_name', "count", 'users')
# добавим столбец с конверсией
start= steps.loc[0,'users']
steps['Конверсия'] = round(steps['users']/start*100, 1)
# Посчитаем конверсию от этапа к этапу
steps['Конверсия в шаг %'] = round(steps['users']/steps['users'].shift()*100, 1)
#добавим вручную первое значение stage_conversion
steps.loc[0, 'Конверсия в шаг %'] = round(steps.loc[0,'users']/start*100, 1)
steps = steps.sort_values(by='count', ascending=False)
steps
| event_name | count | users | Конверсия | Конверсия в шаг % | |
|---|---|---|---|---|---|
| 0 | login | 8194 | 2787 | 100.0 | 100.0 |
| 2 | product_page | 5108 | 1757 | 63.0 | 212.7 |
| 1 | product_cart | 2446 | 826 | 29.6 | 29.6 |
| 3 | purchase | 2406 | 850 | 30.5 | 48.4 |
#построим воронку с процентом перехода на каждый этап относительно начального
fig = go.Figure(go.Funnel(
x = steps['count'],
y = steps['event_name'],
textinfo = "value+percent previous+percent initial"))
fig.update_layout(title='Этапы воронки продаж', title_x = 0.55)
fig.show();
**Наблюдение:**
По итогам анализа воронки продаж было выявлено следующее:
Конверсия (расчет с первого шага):
Создадим сводную таблицу conver_group, сгруппируем данные по группам A/B-теста, в качестве столбцов — названия события, посчитаем количество пользователей и выведем на экран.
conver_group = df_AB_testing.pivot_table(index='event_name', columns='group', values='user_id', aggfunc='nunique').reset_index()
conver_group= conver_group.replace({'event_name':{'login':'1_регистрация', 'product_page':'2_просмотр карточек товара',\
'product_cart':'3_просмотр корзины','purchase':'4_покупка'}})
conver_group = conver_group.sort_values(by='event_name')
conver_group
| group | event_name | A | B |
|---|---|---|---|
| 0 | 1_регистрация | 2082 | 705 |
| 2 | 2_просмотр карточек товара | 1360 | 397 |
| 1 | 3_просмотр корзины | 631 | 195 |
| 3 | 4_покупка | 652 | 198 |
Посмотрим изменение воронки продаж по каждой группе
funnel_A = (group_A.groupby('event_name')['user_id'].nunique().sort_values(ascending=False).to_frame().reset_index()
.rename(columns={'user_id': 'total_users'}))
funnel_A['step'] = pd.Series([0, 1, 3, 2])
funnel_A = funnel_A.sort_values(by='step', ascending=True)
funnel_B = (group_B.groupby('event_name')['user_id'].nunique().sort_values(ascending=False).to_frame().reset_index()
.rename(columns={'user_id': 'total_users'}))
funnel_B['step'] = pd.Series([0, 1, 3, 2])
funnel_B = funnel_B.sort_values(by='step', ascending=True)
funnel_B
| event_name | total_users | step | |
|---|---|---|---|
| 0 | login | 705 | 0 |
| 1 | product_page | 397 | 1 |
| 3 | product_cart | 195 | 2 |
| 2 | purchase | 198 | 3 |
#Построим Stacked Funnel Plot with go.Funnel
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'group_A',
y = funnel_A['event_name'],
x = funnel_A['total_users'],
textinfo = "value+percent initial"))
fig.update_layout(title='Воронка продаж группы "А" и группы "В"', title_x = 0.55)
fig.add_trace(go.Funnel(
name = 'group_B',
orientation = "h",
y = funnel_B['event_name'],
x = funnel_B['total_users'],
textposition = "inside",
textinfo = "value+percent initial"))
fig.show()
**Наблюдение:**
По графику видно, что в группе А конверсия по этапам чуть лучше, чем в группе В. После авторизации в группе А лишь 31% доходит до покупок, в группе В - 28%. Больше всего пользователей отваливается на этапе просмотра карточек товара, к следующему этапу переходит не более 50%. Показатель прехода от 3 этапа к 4 очень позитивный - после просмотра корзины все пользователи переходят к покупкам. Можно заметить,что на 4 этапе в обеих группах процент пользователей больше, чем на предыдущем этапе. Это может говорить о том, что на платформе нестрогая воронка продаж и можно приобрести продукт минуя некоторые этапы.
Согласно ТЗ, Ожидаемый эффект: за 14 дней с момента регистрации в системе пользователи покажут улучшение каждой метрики не менее, чем на 10%. Что же мы имеем по факту:
Как мы видим, ожидаемый эффект не оправдался.
Перед А/В тестом проводят A/A-тест. Если трафик и инструмент проведения A/A-теста не подвели, различий в показателях не будет и поможет определить длительность теста и методику анализа данных. Критерии успешного A/A-теста:
Итого: Контрольная группа почти в 3 раза превышает эксперементальную.
Итого: Согласно ТЗ, предполагалась, что аудитория теста будет сотоять из 15% новых пользователей региона EU. По факту же, в эксперимент попали пользователи из других регионов, пусть и в незначительном количестве, но это тоже может исказить результат эксперимента.
Итого: Изначально предполагалось, что дата окончания эксперимента 2021-01-04. Но по событиям пользователей мы видим, что фактически эксперемент заканчивается 29 декабря- последняя дата наблюдаемой активности пользователей. Время проведения эксперимента выбрано неудачно, так как это новогодние праздники и предновогодняя суета могла повлиять на активность пользователей в этот период. На время проведения теста накладывается маркетинговая кампания, проводимая с 25 декабря
Перед тем как начать A/B-тест, нужно убедиться, что:
Перед началом проведения А/В теста вероятно всего не проводился А/А тест для оценки корректного времени и необходимого количества данных. К сожалению, в данном примере мы столкнулись с проблемой некорректного деления трафика теста, те пользователи были распределены меджу группами неравномерно, что привело к искажению результатов.
1. Проверим по группам
Проверим гипотезу о равенстве долей при помощи Z-критерия. Для этого напишем функцию.
Посчитаем статистическую значимость различия между группами.
Формулировка гипотез:
Нулевая гипотеза(H_0): группа "А" = группа "В" - одинаковыеАльтернативная гипотеза (H_a): группа "А" = группа "В" -" разныеalpha = 0.05 - выберим данный уровень значимости (вероятный порог "необычности")
user_event = df_AB_testing.pivot_table(index = 'group', columns = 'event_name', values = 'user_id',
aggfunc = 'nunique').reset_index()
user_events = user_event.set_index(user_event.columns[0])
user_events
| event_name | login | product_cart | product_page | purchase |
|---|---|---|---|---|
| group | ||||
| A | 2082 | 631 | 1360 | 652 |
| B | 705 | 195 | 397 | 198 |
# Напишем функцию
def st_test(pA_ev, pB_ev, pA_us, pB_us):
# пропорция успехов в первой группе:
pA = pA_ev / pA_us
# пропорция успехов во второй группе:
pB = pB_ev / pB_us
# пропорция успехов в комбинированном датасете:
p_combined = (pA_ev + pB_ev) / (pA_us + pB_us)
# разница пропорций в датасетах
difference = pA - pB
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1 / pA_us + 1 / pB_us))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
return p_value
# Укажем значения по группам А и В в p-value
p_value = st_test(2082, 705, user_events.loc['A'].sum(), user_events.loc['B'].sum())
print('p-значение: ',"{0:.3f}".format(p_value))
alpha = .05
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
p-значение: 0.036 Отвергаем нулевую гипотезу: между долями есть значимая разница
**Наблюдение:**
Получив крайне маленькое значение p-value, мы отвергли Нулевую гипотезу. Таким образом, у нас практически нет вероятности получить одинаковые доли групп "А" и "В".
2. Проверим по каждому событию
total_users = df_AB_testing.groupby('group')['user_id'].nunique()
total_users
group A 2082 B 706 Name: user_id, dtype: int64
Событие login совершили все пользователи каждой группы, поэтому проводить сравнение на данном этапе бессмыслено. Удалим эту строку из таблицы.
conver_group = conver_group.drop(0, 0).reset_index(drop=True)
conver_group
| group | event_name | A | B |
|---|---|---|---|
| 0 | 2_просмотр карточек товара | 1360 | 397 |
| 1 | 3_просмотр корзины | 631 | 195 |
| 2 | 4_покупка | 652 | 198 |
При написании функции применим поправку Бонферрони, чтобы смягчить увеличение вероятности совершения ошибки 1-го рода при многократной проверке
#напишем функцию для проведения А/В теста. В качестве аргумента функции будем передовать названия сравниваемых групп
def stat_test(group1, group2):
for i in conver_group.index:
alpha = .05/len(conver_group) # критический уровень статистической значимости c поправкой Бонферрони
successes = np.array([conver_group[group1][i],conver_group[group2][i]])
trials = np.array([total_users[group1], total_users[group2]])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('Событие: ', conver_group['event_name'][i])
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
print()
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
print()
stat_test('A', 'B')
Событие: 2_просмотр карточек товара p-значение: 1.5371909704686715e-05 Отвергаем нулевую гипотезу: между долями есть значимая разница Событие: 3_просмотр корзины p-значение: 0.1766337419130104 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Событие: 4_покупка p-значение: 0.10281767567786759 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
**Наблюдение:**
По результатам А/В теста значимая разница между группами прослеживается только на этапе просмотра карточек товара. А вот что касается перехода в корзину и покупки товара, то тут значимой разницы не наблюдается. Нет оснований считать, что есть разница в конверсии между контрольной и экспериментальной группой после внедрения новшеств на сайте.
ВЫВОД: